spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { ExternalLink } from 'lucide-react';5import { GraphLink } from '@/components/graph/graph-link';6import { PageHeader, Section, KV, Note } from '@/components/ui/section';7import { Badge, ClaimBadge } from '@/components/ui/badge';8import { EmptyState } from '@/components/ui/empty-state';9import { Freshness } from '@/components/ui/freshness';10import { Pager } from '@/components/ui/pager';11import { SourceBadge } from '@/components/ui/source-badge';12import { ApprovalsTable } from '@/components/data/approvals-table';13import { EvidenceTable } from '@/components/data/evidence-table';14import { TrialTable } from '@/components/data/trial-list';15import { getDrugBySlug, approvalsForDrug, codesForDrug, pipelineForDrug, type DrugCodeRow } from '@/lib/queries/drugs';16import { evidenceForDrug, evidenceForDrugCount, EVIDENCE_PAGE_SIZE } from '@/lib/queries/evidence';17import { trialsForDrug, trialsForDrugCount, TRIAL_PAGE_SIZE } from '@/lib/queries/trials';18import { loadProvenance } from '@/lib/queries/provenance';19import { jsonLd, drugLd } from '@/lib/seo';20import { fmtDate, fmtInt, humanize, phaseLabel } from '@/lib/format';21import { pageInfo } from '@/lib/pagination';22import { str, int, withParams, type SP } from '@/lib/search-params';2324export const revalidate = 3600;2526const CODE_SYSTEMS: Record<string, string> = {27 atc: 'ATC (WHO)',28 din: 'DIN (Health Canada)',29 hc_drug_code: 'DPD drug code',30 unii: 'UNII (FDA GSRS)',31 rxcui: 'RxCUI (RxNorm)',32 ncit: 'NCIt',33 chembl: 'ChEMBL',34 drugbank: 'DrugBank',35 pubchem_cid: 'PubChem CID',36 civic_therapy: 'CIViC therapy',37 ema_product: 'EMA product',38 mhra: 'MHRA',39 tga: 'TGA',40};41function codeSystemLabel(system: string): string {42 return CODE_SYSTEMS[system] ?? humanize(system);43}4445/** External link for code systems with a stable public URL pattern; plain text otherwise. */46function CodeLink({ c, codes }: { c: DrugCodeRow; codes: DrugCodeRow[] }) {47 let url: string | null = null;48 if (c.system === 'atc') url = `https://atcddd.fhi.no/atc_ddd_index/?code=${encodeURIComponent(c.code)}`;49 else if (c.system === 'hc_drug_code') url = `https://health-products.canada.ca/dpd-bdpp/info?lang=eng&code=${encodeURIComponent(c.code)}`;50 else if (c.system === 'din') {51 // The DPD public page is keyed by drug code, not DIN: reuse the hc_drug_code row that carries the same brand label.52 const dc = codes.find((k) => k.system === 'hc_drug_code' && k.label === c.label);53 url = dc ? `https://health-products.canada.ca/dpd-bdpp/info?lang=eng&code=${encodeURIComponent(dc.code)}` : null;54 } else if (c.system === 'chembl') url = `https://www.ebi.ac.uk/chembl/compound_report_card/${encodeURIComponent(c.code)}/`;55 else if (c.system === 'ncit') url = `https://evsexplore.semantics.cancer.gov/evsexplore/concept/ncit/${encodeURIComponent(c.code)}`;56 else if (c.system === 'pubchem_cid') url = `https://pubchem.ncbi.nlm.nih.gov/compound/${encodeURIComponent(c.code)}`;57 else if (c.system === 'drugbank') url = `https://go.drugbank.com/drugs/${encodeURIComponent(c.code)}`;58 if (!url) return <>{c.code}</>;59 return (60 <a className="ci-link inline-flex items-center gap-1" href={url} target="_blank" rel="noopener noreferrer">61 {c.code} <ExternalLink className="h-3 w-3" aria-hidden />62 </a>63 );64}6566export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {67 const d = await getDrugBySlug((await params).slug);68 return d ? { title: `${d.name} — drug`, description: d.description ?? `${d.name}: regulatory approvals by jurisdiction, curated evidence and clinical trials.`, alternates: { canonical: `/drug/${d.slug}` } } : { title: 'Drug' };69}7071export default async function DrugPage({ params, searchParams }: { params: Promise<{ slug: string }>; searchParams: Promise<SP> }) {72 const { slug } = await params;73 const d = await getDrugBySlug(slug);74 if (!d) notFound();75 const sp = await searchParams;76 const [evTotal, tTotal] = await Promise.all([evidenceForDrugCount(d.id), trialsForDrugCount(d.id)]);77 const ev = pageInfo(int(sp, 'evPage', 1, 1, 100_000), EVIDENCE_PAGE_SIZE, evTotal);78 const tp = pageInfo(int(sp, 'tPage', 1, 1, 100_000), TRIAL_PAGE_SIZE, tTotal);79 const aPage = int(sp, 'aPage', 1, 1, 100_000);80 const [approvals, evidence, trials, codes, pipeline] = await Promise.all([approvalsForDrug(d.id), evTotal ? evidenceForDrug(d.id, { page: ev.page, pageSize: ev.pageSize }) : Promise.resolve([]), tTotal ? trialsForDrug(d.id, { page: tp.page, pageSize: tp.pageSize }) : Promise.resolve([]), codesForDrug(d.id), pipelineForDrug(d.id)]);81 const hasCanada = approvals.some((a) => a.jurisdiction === 'CA');82 const prov = await loadProvenance([...approvals.map((a) => a.provenance_id), ...evidence.map((e) => e.provenance_id)]);83 const jurisdictions = [...new Set(approvals.map((a) => a.jurisdiction))].sort();84 const wanted = approvals.length ? str(sp, 'jurisdiction') : '';85 const selected = jurisdictions.includes(wanted) ? wanted : null;86 const shown = selected ? approvals.filter((a) => a.jurisdiction === selected) : approvals;87 const aliases = d.aliases ?? [];88 const current = { jurisdiction: selected ?? '', evPage: ev.page > 1 ? ev.page : '', tPage: tp.page > 1 ? tp.page : '', aPage: aPage > 1 ? aPage : '' };89 const href = (o: Record<string, string | number | null | undefined>, hash?: string) => `/drug/${d.slug}${withParams(current, o)}${hash ? `#${hash}` : ''}`;9091 return (92 <article>93 <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: jsonLd(drugLd({ slug: d.slug, name: d.name, description: d.description, mechanism: d.mechanism, aliases })) }} />94 <PageHeader kicker={`Drug${d.kind ? ` · ${humanize(d.kind)}` : ''}`} title={d.name} lede={d.description ?? undefined}>95 <p className="mt-2 flex flex-wrap items-center gap-2 text-[12.5px]">96 <span className="ci-mono text-ink-3">{d.id}</span>97 <GraphLink type="drug" entityRef={d.slug} />98 {d.ncit_code ? (99 <a className="ci-link inline-flex items-center gap-1" href={`https://evsexplore.semantics.cancer.gov/evsexplore/concept/ncit/${d.ncit_code}`} target="_blank" rel="noopener noreferrer">100 NCIt {d.ncit_code} <ExternalLink className="h-3 w-3" aria-hidden />101 </a>102 ) : null}103 {d.chembl_id ? (104 <a className="ci-link inline-flex items-center gap-1" href={`https://www.ebi.ac.uk/chembl/compound_report_card/${d.chembl_id}/`} target="_blank" rel="noopener noreferrer">105 {d.chembl_id} <ExternalLink className="h-3 w-3" aria-hidden />106 </a>107 ) : null}108 {d.drugbank_id ? (109 <a className="ci-link inline-flex items-center gap-1" href={`https://go.drugbank.com/drugs/${d.drugbank_id}`} target="_blank" rel="noopener noreferrer">110 {d.drugbank_id} <ExternalLink className="h-3 w-3" aria-hidden />111 </a>112 ) : null}113 {d.civic_therapy_id ? (114 <a className="ci-link inline-flex items-center gap-1" href={`https://civicdb.org/therapies/${d.civic_therapy_id}/summary`} target="_blank" rel="noopener noreferrer">115 CIViC <ExternalLink className="h-3 w-3" aria-hidden />116 </a>117 ) : null}118 {d.development_status ? <Badge tone="outline">{humanize(d.development_status)}</Badge> : null}119 </p>120 </PageHeader>121122 <div className="grid gap-8 lg:grid-cols-[1fr_320px]">123 <div className="space-y-8">124 <Section id="approvals" kicker="Regulatory" title={`Approvals (${fmtInt(approvals.length)})`} description="Each record names the authority, jurisdiction, indication text and status. A drug approved in one jurisdiction for one indication is not 'approved' in general.">125 {approvals.length ? (126 <>127 <nav aria-label="Jurisdiction" className="mb-3 flex flex-wrap gap-1.5 text-[12.5px]">128 <Link href={href({ jurisdiction: '' }, 'approvals')} aria-current={!selected ? 'page' : undefined} className="ci-chip">129 All130 </Link>131 {jurisdictions.map((j) => (132 <Link key={j} href={href({ jurisdiction: j }, 'approvals')} aria-current={selected === j ? 'page' : undefined} className="ci-chip ci-mono">133 {j}134 </Link>135 ))}136 </nav>137 <ApprovalsTable rows={shown} prov={prov} showDrug={false} page={aPage} hrefFor={(p) => href({ aPage: p > 1 ? p : '' }, 'approvals')} />138 {hasCanada ? (139 <p className="mt-2 text-[12px] text-ink-3">140 Health Canada records are DIN-level: one row per marketed product (brand, strength, form). The Drug Product Database does not publish indications, so no cancer is stated for these rows, and a cancelled or dormant DIN is the status of that one product — not a withdrawal of the molecule.141 </p>142 ) : null}143 <Freshness dataUpdatedAt={approvals.reduce<Date | string | null>((m, a) => (m == null || String(a.updated_at) > String(m) ? a.updated_at : m), null)} />144 </>145 ) : (146 <EmptyState compact knows={[{ label: 'Curated evidence below', href: '#evidence' }, { label: 'Sources', href: '/sources' }]}>147 No regulatory approval recorded. Absence here is not evidence of absence: only ingested jurisdictions are covered.148 </EmptyState>149 )}150 </Section>151152 <Section id="pipeline" kicker="Derived" title="Development pipeline" description="Most advanced stage across all cancers, then per top-level cancer reached through trial conditions or approval indications. Approval in any ingested jurisdiction outranks trial phase; counts are interventional studies.">153 {pipeline.length ? (154 <>155 <div className="ci-table-wrap">156 <table className="ci-table">157 <thead>158 <tr>159 <th scope="col">Scope</th>160 <th scope="col">Stage</th>161 <th scope="col">Max phase</th>162 <th scope="col" className="num">Active</th>163 <th scope="col" className="num">Recruiting</th>164 <th scope="col" className="num">Phase 3</th>165 <th scope="col" className="num">Trials</th>166 <th scope="col" className="num">Approvals</th>167 <th scope="col">Jurisdictions</th>168 <th scope="col">First approval</th>169 <th scope="col">First trial</th>170 </tr>171 </thead>172 <tbody>173 {pipeline.map((p) => (174 <tr key={p.id}>175 <td className="min-w-[160px]">176 {p.cancer_slug ? (177 <Link className="ci-link" href={`/pipeline?cancer=${p.cancer_slug}`}>178 {p.cancer_name}179 </Link>180 ) : (181 <span className="font-medium">All cancers</span>182 )}183 </td>184 <td>185 <Badge tone={p.stage === 'approved' ? 'ok' : p.stage === 'withdrawn' ? 'danger' : p.stage === 'phase_not_stated' ? 'outline' : 'neutral'}>{humanize(p.stage)}</Badge>186 </td>187 <td className="text-[12.5px] text-ink-2">{p.max_phase ? phaseLabel(p.max_phase) : '—'}</td>188 <td className="num">{fmtInt(p.active_trials)}</td>189 <td className="num">{fmtInt(p.recruiting_trials)}</td>190 <td className="num">{fmtInt(p.phase3_trials)}</td>191 <td className="num">{fmtInt(p.total_trials)}</td>192 <td className="num">{fmtInt(p.approvals)}</td>193 <td className="ci-mono text-[12px]">{p.jurisdictions.length ? p.jurisdictions.join(', ') : '—'}</td>194 <td className="whitespace-nowrap">{fmtDate(p.first_approval_date)}</td>195 <td className="whitespace-nowrap">{p.first_trial_date ? (p.first_trial_date.length >= 10 ? fmtDate(p.first_trial_date) : p.first_trial_date) : '—'}</td>196 </tr>197 ))}198 </tbody>199 </table>200 </div>201 <div className="mt-2 flex flex-wrap items-center gap-1.5 text-[12px] text-ink-3">202 <ClaimBadge kind="computed" />203 <SourceBadge p={{ sourceSlug: 'clinicaltrials', layer: 'derived', note: 'Inputs: interventional trials linking this drug (trial_interventions × trial_conditions) and its approval records.' }} />204 <span className="ci-mono">{pipeline[0]!.formula_version}</span>205 <Link className="ci-link" href="/methodology/pipeline">206 Stage rules207 </Link>208 </div>209 <Freshness dataUpdatedAt={pipeline[0]!.computed_at} />210 </>211 ) : (212 <EmptyState compact>No registered interventional trial or approval record places this drug in the pipeline yet.</EmptyState>213 )}214 </Section>215216 <Section id="evidence" kicker="Curated evidence" title={`Clinical evidence (${fmtInt(evTotal)})`} description={`CIViC items in which this therapy appears, grouped by cancer context, then molecular profile. ${EVIDENCE_PAGE_SIZE} items per page.`}>217 {evidence.length ? (218 <>219 <EvidenceTable220 items={evidence}221 prov={prov}222 showCancer223 summary={224 <>225 Showing {fmtInt(ev.from)}–{fmtInt(ev.to)} of {fmtInt(evTotal)} evidence items226 </>227 }228 />229 <Pager total={evTotal} pageSize={ev.pageSize} page={ev.page} hrefFor={(p) => href({ evPage: p > 1 ? p : '' }, 'evidence')} label="Evidence pages" noun="evidence items" />230 </>231 ) : (232 <EmptyState compact>No curated evidence item mentions this therapy yet.</EmptyState>233 )}234 </Section>235236 <Section id="trials" kicker="Clinical trials" title={`Trials with this intervention (${fmtInt(tTotal)})`} description={tTotal ? `Most recently updated first, ${TRIAL_PAGE_SIZE} per page.` : undefined}>237 {trials.length ? (238 <>239 <TrialTable240 rows={trials}241 summary={242 <>243 Showing {fmtInt(tp.from)}–{fmtInt(tp.to)} of {fmtInt(tTotal)} studies244 </>245 }246 />247 <Pager total={tTotal} pageSize={tp.pageSize} page={tp.page} hrefFor={(p) => href({ tPage: p > 1 ? p : '' }, 'trials')} label="Trial pages" noun="studies" />248 </>249 ) : (250 <EmptyState compact>No registered study lists this drug as an intervention yet.</EmptyState>251 )}252 </Section>253 </div>254255 <aside className="space-y-8">256 <Section id="record" kicker="Molecule" title="Record" level={3}>257 <KV258 items={[259 { k: 'Kind', v: d.kind ? humanize(d.kind) : null },260 { k: 'Mechanism', v: d.mechanism },261 { k: 'Targets', v: d.target_gene_ids.length ? `${d.target_gene_ids.length} gene${d.target_gene_ids.length === 1 ? '' : 's'}` : null },262 { k: 'UNII', v: d.unii ? <span className="ci-mono">{d.unii}</span> : null },263 { k: 'PubChem CID', v: d.pubchem_cid ? <a className="ci-link ci-mono" href={`https://pubchem.ncbi.nlm.nih.gov/compound/${d.pubchem_cid}`} target="_blank" rel="noopener noreferrer">{d.pubchem_cid}</a> : null },264 { k: 'Aliases', v: aliases.length ? aliases.slice(0, 30).join(', ') + (aliases.length > 30 ? ` … (+${aliases.length - 30})` : '') : null },265 ]}266 />267 <Freshness dataUpdatedAt={d.updated_at} />268 </Section>269 <Section id="identifiers" kicker="Identifiers" title={`Codes (${fmtInt(codes.length)})`} level={3} description="Upstream identifiers kept as first-class codes; each links to its registry where a stable public URL exists.">270 {codes.length ? (271 <div className="ci-table-wrap">272 <table className="ci-table">273 <thead>274 <tr>275 <th scope="col">System</th>276 <th scope="col">Code</th>277 <th scope="col">Label</th>278 </tr>279 </thead>280 <tbody>281 {codes.map((c) => (282 <tr key={c.id}>283 <td className="whitespace-nowrap text-[12.5px] text-ink-2">{codeSystemLabel(c.system)}</td>284 <td className="ci-mono text-[12px]">285 <CodeLink c={c} codes={codes} />286 </td>287 <td className="max-w-[200px] text-[12.5px] text-ink-2">{c.label ?? '—'}</td>288 </tr>289 ))}290 </tbody>291 </table>292 </div>293 ) : (294 <EmptyState compact>No cross-reference code recorded yet beyond the record identifiers above.</EmptyState>295 )}296 </Section>297 <Note tone="warn">Regulatory status is jurisdiction-specific and time-bound. Nothing on this page is a treatment recommendation.</Note>298 </aside>299 </div>300 </article>301 );302}303